﻿using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Configuration;
using System.Globalization;
using System.Net;
using System.Threading;
using Selenium;

namespace Quail
{
    /// <summary>
    /// The TestSession for the current test run
    /// </summary>
    public class SeleniumTestSession : TestSession
    {
		private static readonly Hashtable HashHosts = new Hashtable();
		private static readonly ConcurrentDictionary<string, Mutex> HostMutexes = new ConcurrentDictionary<string, Mutex>(StringComparer.OrdinalIgnoreCase);

		private bool _isStopped;
        private bool _captureNetworkTraffic;
        private bool _isAttached;
        private RemoteControlHost _host;

        /// <summary>
        /// Initializes a new instance of the TestSession class
        /// </summary>
        /// <param name="sessionName">Name of test session</param>
        public SeleniumTestSession(string sessionName):base(sessionName)
        {
            RcHost = new RemoteControlHost("localhost", 4444);
            BrowserType = BrowserTypes.FireFox;
            _isStopped = true;

        	var setting = ConfigurationManager.AppSettings["DefaultQuailStartupTimeoutInMs"];
        	int settingvalue;

        	DefaultSeleniumStartupTimeout = !string.IsNullOrEmpty(setting) && int.TryParse(setting, out settingvalue)
        	                                	? TimeSpan.FromMilliseconds(settingvalue)
        	                                	: TimeSpan.FromMinutes(1);
        }

        /// <summary>
        /// Gets a value indicating whether the browser is started
        /// </summary>
        public bool IsStarted
        {
            get
            {
                return SeleniumInterface != null && !_isStopped;
            }
        }

        /// <summary>
        /// Gets the thread id that started the test session
        /// </summary>
        public int StartingThreadId
        {
            get; private set;
        }

        /// <summary>
        /// Gets or sets the Selenium Remote Control host
        /// </summary>
        public RemoteControlHost RcHost
        {
            get
            {
                return _host;
            }
            
            set
            {
                if (_host != null)
                    _host.Dispose();
                _host = value;
            }
        }

        /// <summary>
        /// Gets or sets the browser to use for the test session
        /// </summary>
        public BrowserTypes BrowserType
        {
            get; set;
        }


		/// <summary>
		/// Gets or sets the default timeout waiting for Selenium to start (default: DefaultQuailStartupTimeoutInMs appsetting or 1 minute)
		/// </summary>
		public TimeSpan DefaultSeleniumStartupTimeout { get; set; }

        /// <summary>
        /// Gets the current browser's Url
        /// </summary>
        public override string CurrentUrl
        {
            get
            {
                //// the _threadId stuff is broken for SAJParallelTestManager tests
                ////if (_threadId != 0 && _threadId != Thread.CurrentThread.ManagedThreadId)
                ////    throw new Exception("Constructor thread doesn't match thread used in CurrentUrl!");

                if (IsStarted)
                    return base.CurrentUrl;

                return null;
            }
        }

        /// <summary>
        /// Gets the current Selenium interface for the test session
        /// </summary>
        public ISelenium SeleniumInterface
        {
            get; private set;
        }

        /// <summary>
        /// Gets the current Selenium command procesor for the test session
        /// </summary>
        public QuailHttpCommandProcessor Processor
        {
            get; private set;
        }

    	private bool _canCacheBaseUrl;
    	private string _baseUrl;

        /// <summary>
        /// Gets or sets the BaseUrl used for relative paths in selenium
        /// </summary>
        public override string BaseUrl
        {
            get
            {
				if (!_canCacheBaseUrl)
				{
					_baseUrl = null;
					return Processor.GetString("getBaseUrl", new string[] {});
				}

				return _baseUrl ?? (_baseUrl = Processor.GetString("getBaseUrl", new string[] { }));
            }

            set
            {
            	_baseUrl = value;
				Processor.DoCommand("changeBaseUrl", new[] { value });
            }
        }

        /// <summary>
        /// Gets or sets a value indicating whether to capture network traffic.  Must be set before the browser is started
        /// </summary>
        public override bool CaptureNetworkTraffic
        {
            get
            {
                return _captureNetworkTraffic;
            }

            set
            {
                if (!_isStopped)
                    throw new Exception("You can't toggle the CaptureNetworkFlag while the test session is running!");
                _captureNetworkTraffic = value;
            }
        }

        /// <summary>
        /// Gets the name of the browser to pass to Selenium RC
        /// </summary>
        private string BrowserName
        {
            get
            {
                switch (BrowserType)
                {
                    case BrowserTypes.FireFox:
                        return "*firefox";
                    case BrowserTypes.Ie:
                        // note: if you get "No such interface supported" when launching IE, try: regsvr32 actxprxy.dll   and regsvr32 shdocvw.dll
                        // see: http://www.dcs-media.com/desdev/javascript/javascript-error-no-such-interface-supported-solved.aspx
                        return "*iehta";
                    case BrowserTypes.Opera:
                        return "*opera";
                    case BrowserTypes.Safari:
                        return "*safari";
                    case BrowserTypes.GoogleChrome:
                        return "*googlechrome";
                    default:
                        return "*firefox";
                }
            }
        }


        /// <summary>
        /// Disposes the test shell by stopping it and stopping the logger
        /// </summary>
        protected override void Dispose(bool disposing)
        {
            base.Dispose(disposing);

            if (disposing)
            {
                if (_host != null)
                {
                    _host.Dispose();
                    _host = null;
                }
            }
        }

        /// <summary>
        /// Attaches the test session to an existing selenium session
        /// </summary>
        /// <param name="sessionid">The session id of the existing selenium remote control session</param>
        /// <returns>The web browser</returns>
        public virtual IWebBrowser Attach(string sessionid)
        {
            var task = ActiveTask;
            task.AddLog(LogStatus.Startup, "Attaching to an existing session: {0}, {1}", RcHost, sessionid);
            Processor = CreateProcessor(RcHost.Host, RcHost.Port, BrowserName, string.Empty);
            Processor.SessionId = sessionid;
            SeleniumInterface = CreateSelenium(Processor);
            StartingThreadId = Thread.CurrentThread.ManagedThreadId;

            _isStopped = false;
            _isAttached = true;
            if (Browser == null)
                Browser = CreateBrowser();
            SeleniumInterface.SetTimeout(Browser.Timeout.ToString(CultureInfo.InvariantCulture));

            return Browser;
        }

        /// <summary>
        /// Starts the browser session
        /// </summary>
        /// <param name="url">The url to open</param>
        /// <returns>The new web browser object</returns>
        public override IWebBrowser StartBrowser(string url)
        {
            var task = ActiveTask;

            // start/stop selenium only one at a time per rc host to avoid browser startup issues
			//lock (GetHostLocker(RcHost.Host))

        	task.AddLogAndUrl(
        		LogStatus.Startup,
        		string.Format("Selenium server: {0}, browser: {1}, url: {2}, startuptimeout: {3}",
        		              RcHost,
        		              BrowserName,
        		              url,
        		              DefaultSeleniumStartupTimeout),
        		string.Empty);

            int failcount = 0;

            for (;;)
            {
				try
				{
					// wait for the rc to startup.
					Processor = CreateProcessor(RcHost.Host, RcHost.Port, BrowserName, url);

					var timeoutms = (int)DefaultSeleniumStartupTimeout.TotalMilliseconds;
					SeleniumInterface = CreateSelenium(Processor);

					var oldtimeout = Processor.DefaultHttpTimeout;
					if (oldtimeout < timeoutms)
						Processor.DefaultHttpTimeout = timeoutms;

					using (LockSeleniumHost(RcHost.Host))
					{
						if (CaptureNetworkTraffic && SeleniumInterface is DefaultSelenium)
							((DefaultSelenium) SeleniumInterface).Start("captureNetworkTraffic=true");
						else
							SeleniumInterface.Start();
					}

					Processor.DefaultHttpTimeout = oldtimeout;
					StartingThreadId = Thread.CurrentThread.ManagedThreadId;

					break;
				}
				catch (WebException timeoutex)
				{
					if (timeoutex.Message.Contains("The operation has timed out") || timeoutex.Message.Contains("The request was aborted"))
					{
						task.AddLogAndUrl(LogStatus.Information, "Restarting browser due to startup failure", string.Empty);

						try
						{
							using (LockSeleniumHost(RcHost.Host))
								SeleniumInterface.Stop();
						}
						catch (Exception ex)
						{
							task.AddLogAndUrl(LogStatus.Information, "Stopping browser during restart failed: " + ex.Message, string.Empty);
						}

						// try up to 3 times to restart the test session
						failcount++;
						if (failcount == 3)
							throw;
					}
					else
						throw;
				}
            }

            _isStopped = false;
            if (Browser == null)
                Browser = CreateBrowser();
            
            base.RunOnBeforeStartBrowser();

            SeleniumInterface.SetTimeout(Browser.Timeout.ToString(CultureInfo.InvariantCulture));

			for (var i = 0; i < 3; i++)
			{
				Browser.Navigate(url);
				if (BrowserType == BrowserTypes.GoogleChrome)
				{
					Browser.Sleep(1000);
					Browser.SyncAnyPage();
				}

				if (Browser.Location.IndexOf("/selenium-server/core/Blank.html", StringComparison.Ordinal) < 0)
					break;

				task.AddLog(LogStatus.Information, "Browser stuck on Selenium startup page.  Trying navigate again.");
			}

        	_canCacheBaseUrl = true;
        	return Browser;
        }

        /// <summary>
        /// Stops the browser and the logger
        /// </summary>
        public override void Stop()
        {
            if (IsStarted)
            {
                _isStopped = true;
                try
                {
                    if (!_isAttached)
                    {
                        // start/stop selenium only one at a time per rc host to avoid browser startup issues
						Processor.DefaultHttpTimeout = (int)DefaultSeleniumStartupTimeout.TotalMilliseconds;

						using (LockSeleniumHost(RcHost.Host))
						//lock (GetHostLocker(RcHost.Host))
                        {
                            // Monitor.Enter(locker);
                            SeleniumInterface.Stop();

                            // release the lock after 5 seconds
                            // wait 5 seconds for Firefox to tear down see: http://support.mozilla.com/en-US/kb/Firefox+is+already+running+but+is+not+responding
                            //ThreadPool.RegisterWaitForSingleObject(NeverSetEvent, (l, t) => Monitor.Exit(l), locker, 5000, true);
                            // Thread.Sleep(5000);
                        }
                    }
                }
                catch (Exception ex)
                {
					if (RootTask != null)
						RootTask.AddLogAndUrl(LogStatus.Warning, "Failure shutting down selenium: " + ex.Message, string.Empty);
                }

                _isAttached = false;
                StartingThreadId = 0;
            }
        }

        /// <summary>
        /// Processes a SeleniumException
        /// </summary>
        /// <param name="se">The SeleniumException</param>
        /// <returns>What to do about it</returns>
        public ExceptionHandleResult ExceptionHandle(SeleniumException se)
        {
            // log it or something
            if (RootTask != null)
                ActiveTask.AddLog(LogStatus.Fail, se.ToString());
            return ExceptionHandleResult.Abort;
        }

		/// <summary>
		/// Closes a window on the remote host
		/// </summary>
		/// <param name="windowName">The window name to close</param>
		/// <param name="delay">how long to delay before closing it</param>
		public void CloseWindow(string windowName, int delay)
		{
			var url = string.Format("http://{0}:{1}/?killwindow={2}&delay={3}", RcHost.Host, 4443, windowName, delay);
			var req = WebRequest.Create(url);
			req.GetResponse();
			// no need to inspect the response, we're not returning anything from QuailSeleniumManager(Service)
		}

		/// <summary>
        /// Creates a new ICommandProcessor object
        /// </summary>
        /// <param name="host">The remote control host</param>
        /// <param name="port">The remote control port</param>
        /// <param name="browser">The name of the browser to open</param>
        /// <param name="defaultUrl">The starting Url</param>
        /// <returns>A new object that implements the ICommandProcessor interface</returns>
        private static QuailHttpCommandProcessor CreateProcessor(string host, int port, string browser, string defaultUrl)
        {
            return new QuailHttpCommandProcessor(host, port, browser, defaultUrl);
        }

        /// <summary>
        /// Creates a new Selenium object
        /// </summary>
        /// <param name="proc">The command processor</param>
        /// <returns>A new instance of an object that implements ISelenium</returns>
        protected virtual ISelenium CreateSelenium(ICommandProcessor proc)
        {
            return new DefaultSelenium(proc);
        }

		private static object GetHostLocker(string host)
		{
			lock (HashHosts)
			{
				host = host.ToUpper();
				var o = HashHosts[host];
				if (o != null)
					return o;

				o = new object();
				HashHosts[host] = o;

				return o;
			}
		}

		// [Obsolete("This method was found to cause timeouts, it did not solve selenium startup problems", true)]
		private static IDisposable LockSeleniumHost(string host)
		{
			var mutex = HostMutexes.GetOrAdd(host, key => new Mutex(false, "Global\\QuailHost" + host.Replace("\\", "-")));

			if (!mutex.WaitOne(TimeSpan.FromMinutes(5)))
				throw new TimeoutException("Timed out waiting for access to host mutex for " + host);

			return new HostLock(mutex);
		}

		private class HostLock : IDisposable
		{
			private readonly Mutex _hostMutext;
			public HostLock(Mutex mutex)
			{
				_hostMutext = mutex;
			}

			public void Dispose()
			{
				_hostMutext.ReleaseMutex();
			}
		}
    }
}
